<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
  import {
    type ControlledDocument,
    type DocumentMeta,
    type DocumentSpace,
    type DocumentSpaceType,
    type Project,
    type ProjectDocument,
    ProjectDocumentTree,
    getDocumentName
  } from '@hcengineering/controlled-documents'
  import { type Doc, type Ref, type Space, WithLookup } from '@hcengineering/core'
  import { getResource } from '@hcengineering/platform'
  import { createQuery, getClient } from '@hcengineering/presentation'
  import {
    type Action,
    IconEdit,
    Label,
    closeTooltip,
    getPlatformColorForTextDef,
    navigate,
    themeStore
  } from '@hcengineering/ui'
  import { ActionGroup } from '@hcengineering/view'
  import { TreeItem, TreeNode, getActions as getContributedActions } from '@hcengineering/view-resources'

  import { getDocumentIdFromFragment, getProjectDocumentLink } from '../../navigation'
  import {
    canCreateChildDocument,
    canCreateChildFolder,
    createDocument,
    createDocumentHierarchyQuery,
    createFolder,
    getLatestProjectId,
    isEditableProject,
    moveDocument,
    moveDocumentAfter,
    moveDocumentBefore
  } from '../../utils'
  import ProjectSelector from '../project/ProjectSelector.svelte'
  import DocHierarchyLevel from './DocHierarchyLevel.svelte'

  import documents from '../../plugin'

  import DropArea from './DropArea.svelte'
  import DropMarker from './DropMarker.svelte'

  export let space: DocumentSpace
  export let currentSpace: Ref<Space> | undefined
  export let currentFragment: string | undefined
  export let getActions: (space: Space) => Promise<Action[]> = async () => []
  export let deselect: boolean = false
  export let forciblyСollapsed: boolean = false

  const client = getClient()

  let spaceType: DocumentSpaceType | undefined
  let pressed: boolean = false

  const spaceTypeQuery = createQuery()
  $: spaceTypeQuery.query(documents.class.DocumentSpaceType, { _id: space.type }, (result) => {
    ;[spaceType] = result
  })

  $: selected = getDocumentIdFromFragment(currentFragment ?? '')

  let project: Ref<Project> = documents.ids.NoProject
  $: void selectProject(space)

  let tree = new ProjectDocumentTree()

  const query = createDocumentHierarchyQuery()
  $: if (document !== undefined && project !== undefined) {
    query.query(space._id, project, (data) => {
      tree = data
    })
  }

  let selectedControlledDoc: ControlledDocument | undefined = undefined

  $: if (selected !== undefined) {
    void client
      .findOne(
        documents.class.ProjectDocument,
        { _id: selected },
        {
          lookup: {
            document: documents.class.ControlledDocument
          }
        }
      )
      .then((result) => {
        if (result !== undefined) {
          selectedControlledDoc = result.$lookup?.document as ControlledDocument
        } else {
          // There's some issue with resolving which needs to be fixed later
          void client
            .findOne(documents.class.ControlledDocument, { _id: selected as unknown as Ref<ControlledDocument> })
            .then((result) => {
              selectedControlledDoc = result
            })
        }
      })
  } else {
    selectedControlledDoc = undefined
  }

  async function selectProject (space: DocumentSpace): Promise<void> {
    project = (await getLatestProjectId(space._id, true)) ?? documents.ids.NoProject
  }

  function handleDocumentSelected (doc: WithLookup<ProjectDocument>): void {
    if (doc.$lookup?.document !== undefined) {
      const loc = getProjectDocumentLink(doc.$lookup?.document, doc.project)
      navigate(loc)
    }
  }

  async function getSpaceActions (space: DocumentSpace): Promise<Action[]> {
    const actions = await getActions(space)

    if (
      spaceType?.projects === true &&
      (await isEditableProject(project)) &&
      (await canCreateChildDocument(space, true))
    ) {
      actions.push({
        icon: documents.icon.NewDocument,
        label: documents.string.CreateDocument,
        group: 'create',
        action: async () => {
          await createDocument(space)
        }
      })
    }

    if (
      spaceType?.projects === true &&
      (await isEditableProject(project)) &&
      (await canCreateChildFolder(space, true))
    ) {
      actions.push({
        icon: documents.icon.Folder,
        label: documents.string.CreateFolder,
        group: 'create',
        action: async () => {
          await createFolder(space)
        }
      })
    }

    return orderActions(actions)
  }

  async function getDocumentActions (obj: Doc): Promise<Action[]> {
    const result: Action[] = []
    const extraActions = await getContributedActions(client, obj)
    for (const act of extraActions) {
      result.push({
        icon: act.icon ?? IconEdit,
        label: act.label,
        group: act.context.group,
        action: async (ctx: any, evt: Event) => {
          const impl = await getResource(act.action)
          await impl(obj, evt, act.actionProps)
        }
      })
    }

    return orderActions(result)
  }

  function orderActions (actions: Action[]): Action[] {
    const order: Record<ActionGroup, number> = {
      create: 1,
      edit: 2,
      copy: 3,
      associate: 4,
      tools: 5,
      other: 6,
      remove: 7
    }

    actions.sort((a, b) => order[(a.group as ActionGroup) ?? 'other'] - order[(b.group as ActionGroup) ?? 'other'])

    return actions
  }

  let parent: HTMLElement
  let draggedItem: Ref<DocumentMeta> | undefined = undefined
  let draggedOver: Ref<DocumentMeta> | undefined = undefined
  let draggedOverPos: 'before' | 'after' | undefined = undefined
  let draggedOverTop: number = 0
  let cannotDropTo: Ref<DocumentMeta>[] = []

  function canDrop (object: Ref<DocumentMeta>, target: Ref<DocumentMeta>): boolean {
    if (object === target) return false
    if (cannotDropTo.includes(target)) return false

    return true
  }

  function onDragStart (event: DragEvent, object: Ref<DocumentMeta>): void {
    // no prevent default to leverage default rendering
    // event.preventDefault()
    if (event.dataTransfer === null || event.target === null) {
      return
    }

    cannotDropTo = [object, ...tree.descendantsOf(object)]

    event.dataTransfer.effectAllowed = 'move'
    event.dataTransfer.dropEffect = 'move'
    draggedItem = object

    closeTooltip()
  }

  function getDropPosition (event: DragEvent): { pos: 'before' | 'after' | undefined, top: number } {
    const parentRect = parent.getBoundingClientRect()
    const targetRect = (event.target as HTMLElement).getBoundingClientRect()
    const dropPosition = event.clientY - targetRect.top

    const before = dropPosition >= 0 && dropPosition < targetRect.height / 6
    const after = dropPosition <= targetRect.height && dropPosition > (5 * targetRect.height) / 6

    const pos = before ? 'before' : after ? 'after' : undefined
    const top = pos === 'before' ? targetRect.top - parentRect.top - 1 : targetRect.bottom - parentRect.top - 1

    return { pos, top }
  }

  function onDragOver (event: DragEvent, object: Ref<DocumentMeta>): void {
    event.preventDefault()
    // this is an ugly solution to control drop effect
    // we drag and drop elements that are in the depth of components hierarchy
    // so we cannot access them directly
    if (!(event.target as HTMLElement).draggable) return
    if (event.dataTransfer === null || event.target === null || draggedItem === object) {
      return
    }

    if (draggedItem !== undefined && canDrop(draggedItem, object)) {
      event.dataTransfer.dropEffect = 'move'
      draggedOver = object

      const { pos, top } = getDropPosition(event)
      draggedOverPos = pos
      draggedOverTop = top
    } else {
      event.dataTransfer.dropEffect = 'none'
    }
  }

  function onDragEnd (event: DragEvent): void {
    event.preventDefault()
    draggedItem = undefined
    draggedOver = undefined
    draggedOverPos = undefined
  }

  function onDrop (event: DragEvent, object: Ref<DocumentMeta>): void {
    event.preventDefault()
    if (event.dataTransfer === null) {
      return
    }
    if (draggedItem !== undefined && canDrop(draggedItem, object)) {
      const doc = tree.bundleOf(draggedItem)?.ProjectMeta[0]
      const target = tree.bundleOf(object)?.ProjectMeta[0]

      if (doc !== undefined && target !== undefined && doc._id !== target._id) {
        if (object === documents.ids.NoParent) {
          void moveDocument(doc, doc.space)
        } else if (target !== undefined) {
          const { pos } = getDropPosition(event)
          if (pos === 'before') {
            void moveDocumentBefore(doc, target)
          } else if (pos === 'after') {
            void moveDocumentAfter(doc, target)
          } else if (doc.parent !== object) {
            void moveDocument(doc, target.space, target)
          }
        }
      }
    }
    draggedItem = undefined
    draggedOver = undefined
  }
</script>

<div bind:this={parent} class="flex-col relative">
  {#if draggedOver === documents.ids.NoParent}
    <DropArea />
  {/if}

  {#if draggedOver && draggedOverPos}
    <DropMarker top={draggedOverTop} />
  {/if}

  <TreeNode
    _id={space?._id}
    folderIcon
    iconProps={{
      fill: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
    }}
    title={space.name}
    highlighted={space._id === currentSpace && currentFragment !== undefined && !deselect}
    visible={(space._id === currentSpace && currentFragment !== undefined && !deselect) || forciblyСollapsed}
    showMenu={pressed}
    {forciblyСollapsed}
    actions={() => getSpaceActions(space)}
    type={'nested'}
    draggable
    on:drop={(evt) => {
      onDrop(evt, documents.ids.NoParent)
    }}
    on:dragover={(evt) => {
      onDragOver(evt, documents.ids.NoParent)
    }}
    on:dragstart={(evt) => {
      evt.preventDefault()
    }}
  >
    <svelte:fragment slot="extra">
      {#if spaceType?.projects === true}
        <ProjectSelector
          value={project}
          space={space?._id}
          maxWidth={'6rem'}
          kind={'ghost'}
          size={'x-small'}
          showDropdownIcon
          bind:pressed
          on:change={(evt) => {
            project = evt.detail
          }}
        />
      {/if}
    </svelte:fragment>

    {@const root = tree.childrenOf(documents.ids.NoParent)}
    {#if root.length > 0}
      <DocHierarchyLevel
        {tree}
        documentIds={root}
        {selected}
        getMoreActions={getDocumentActions}
        on:selected={(e) => {
          handleDocumentSelected(e.detail)
        }}
        {onDragStart}
        {onDragEnd}
        {onDragOver}
        {onDrop}
        {draggedItem}
        {draggedOver}
      />
    {:else}
      <div class="pseudo-element flex-row-center content-dark-color text-md nowrap">
        <Label label={documents.string.NoDocuments} />
      </div>
    {/if}

    <svelte:fragment slot="visible">
      {#if (selected || forciblyСollapsed) && selectedControlledDoc}
        {@const doc = selectedControlledDoc}
        <TreeItem
          _id={doc._id}
          icon={documents.icon.Document}
          iconProps={{
            fill: 'currentColor'
          }}
          title={getDocumentName(doc)}
          actions={() => getDocumentActions(doc)}
          selected
          isFold
          empty
          forciblyСollapsed
        />
      {/if}
    </svelte:fragment>
  </TreeNode>
</div>

<style lang="scss">
  .pseudo-element {
    height: 2rem;
    margin: 0 0.75rem;
    padding-left: 2.5rem;
    padding-right: 0.75rem;
  }
</style>
